长安链 Chainmaker 体验记录
本文主要记录了在体验长安链 Chainmaker 过程中遇到的一些问题。如:单机部署管理后台和区块链浏览器;修复 InsertBlockAndTx 卡死;Node.js SDK 不支持国密。记录了针对这些问题的分析以及一些可以尝试的解决思路。
2023-09-11
本次体验长安链 Chainmaker 的过程中主要涉及到了以下几个部分的内容:
使用的长安链版本为 v2.3.0,所有操作均按照官方文档中的步骤进行。在体验过程中,问题主要出现在第一、第三和第六上,具体的问题会在后面说明。
后续的内容分成主要分成三个部分:第一部分介绍体验过程中遇到的三个问题;第二部分提供单机部署管理后台和区块链浏览器的方法;第三部分简单介绍了如何实现一个简单的 go-sdk API 服务。
相关代码已上传 Github。
遇到的问题
按照官方文档的操作步骤基本上都能够达到预期的目标,但是在体验过程中还是遇到了一下无法在文档及问题列表中找到解决方案的问题,后续内容主要是对这些问题进行介绍。
问题一、单机部署管理后台和区块链浏览器
原本计划是直接使用长安链官方提供管理后台和区块链浏览器的 docker-compose 启动服务,并在宿主机上使用 Nginx 通过子域名反向代理到对应的服务。但是部署完成之后发现两个子域名都只能访问到管理后台应用。
通过查看 management-web 和 explorer-web 的 Dockerfile 后发现,这两个 web 应用镜像在容器内部已经使用了 Nginx 作为 web 应用服务器,并且 management-web 的 docker-compose 的中的端口映射是
80:80
为了能够使用宿主机的 Ningx 代理管理后台和区块链浏览器,需要修改管理后台服务的端口映射,因此将官方提供的 management 和 explorer 的 docker-compose 文件进行合并,并将
cm_mgmt_server
9995:80
具体实现可以参见后文。
问题二、管理后台无法订阅部署了智能合约的区块链
使用管理后台生成部署材料并进行部署,并在管理后台订阅了区块链网络。但在提交部署智能合约的时候出现了管理后台服务长时间无法访问,然后自动重启的问题。
查看日志后发现管理后台后端在调用插入
InsertBlockAndTx
Transaction
src/sync/resolver.go
transaction.ContractParams
json.unmarshal
Transaction.ContractParams
问题三、Node.js 的 SDK 不支持国密 SSL
尝试使用长安链提供的 Node.js SDK 调用智能合约,经测试后发现如果使用国密证书进行调用会提示证书不支持(unsupported)。查询后发现目前只有 go-sdk 和 java-sdk 实现了国密 SSL。
为了使用国密所以选择使用 go-sdk
部署管理后台和区块链浏览器
服务器运行环境:
- Debian@12;
- 安装了 docker 及 docker-compose;
ssh 到服务器中并初始化工程目录:
mkdir -p /src/chainmaker
mkdir -p /data/chainmaker/{management,explorer}
初始化项目
在本地工作目录中按以下的脚本初始化一个工程项目:
# 创建项目目录并初始化
mkdir chainmaker && cd chainmaker && git init
# 以 v2.3.0 为例
# 将 management-backend 作为 submodule 以便避免"问题二"
git submodule add https://git.chainmaker.org.cn/chainmaker/management-backend.git management-backend
pushd management-backend
git checkout tags/$VERSION -b $VERSION
popd
# 新建两个目录用于保存数据库和后端配置
mkdir {management_configs,explorer_configs}
避免"问题二"
注释掉
management-backend/src/sync/resolver.go
修改之后的问题是在管理后台的区块链浏览器中无法查看调用合约时的参数,但如果部署了区块链浏览器其实并不是一个很大的问题。同时在调试中发现,只有部署或升级智能合约时的
transaction.ContractParams
更新默认配置
在
management_configs
.env
MYSQL_ROOT_PASSWORD=<管理后台 Mysql Root Password>
MYSQL_USER=chainmaker
MYSQL_PASSWORD=<chainmaker_mgmt 密码>
MYSQL_DATABASE=chainmaker_mgmt
MYSQL_TCP_PORT=3306
management_configs.env
在同一个文件夹下添加
config.yml
web:
address: 0.0.0.0
port: 9999
cross_domain: true
session_age: 86400
captcha:
height: 80
width: 200
noise_count: 5
length: 4
# 错误提示语言,取值:0 - 英文,1 - 中文
errmsg_lang: 1
# 加载链信息间隔时间,单位:秒
load_period_seconds : 60
# 默认 admin 用户密码,和重置用户密码
password: <管理后台密码>
# 日志 agent 的端口
agent_port: 22301
# 上报日志链接
report_url: https://bugreport.chainmaker.org.cn/v1/reportLogs
db:
host: cm_db
port: 3306
database: chainmaker_mgmt
user: chainmaker
passwd: <chainmaker_mgmt 密码>
management_configs/config.yml
对区块链浏览器的配置同理,在
explorer_configs
.env
config.yml
MYSQL_ROOT_PASSWORD=<区块链浏览器 Mysql Root Password>
MYSQL_USER=chainmaker
MYSQL_PASSWORD=<chainmaker_explorer 密码>
MYSQL_DATABASE=chainmaker_explorer
MYSQL_TCP_PORT=3306
explore_configs.env
web:
address: 0.0.0.0
port: 9997
cross_domain: true
node:
# 链和节点更新时间
update_time: 30
# 节点断开连接时间和新增链时间
sync_time: 30
chain:
show_config: true
db:
host: cm_explorer_db
port: 3307
database: chainmaker_explorer
user: chainmaker
passwd: <chainmaker_explorer 密码>
explorer_configs/config.yml
使用 docker-compose 运行服务
添加
docker-compose.yml
version: "3.9"
services:
cm_db:
container_name: "cm_db"
image: mysql:5.7
volumes:
- /data/chainmaker/mgmt:/var/lib/mysql
restart: always
env_file:
- management_configs/.env
command:
[
"mysqld",
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_unicode_ci",
"--max_allowed_packet=200M",
]
cm_mgmt_server:
container_name: "cm_mgmt_server"
depends_on:
- cm_db
build:
context: management-backend
dockerfile: Dockerfile
image: management-backend:v2.3.0
volumes:
- /src/chainmaker/management_configs:/chainmaker-management/configs
ports:
- "9999:9999"
restart: always
cm_mgmt_web:
container_name: "cm_mgmt_web"
depends_on:
- cm_mgmt_server
image: chainmakerofficial/management-web:v2.3.0
ports:
- "9995:80"
restart: always
cm_explorer_db:
container_name: "cm_explorer_db"
image: mysql:5.7
volumes:
- /data/chainmaker/explorer:/var/lib/mysql
restart: always
env_file:
- explorer_configs/.env
command:
[
"mysqld",
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_unicode_ci",
"--max_allowed_packet=200M",
]
cm_explorer_server:
container_name: "cm_explorer_server"
image: chainmakerofficial/explorer-backend:v2.3.0
volumes:
- /src/chainmaker/explorer_configs:/chainmaker-explorer-backend/configs
depends_on:
- cm_explorer_db
ports:
- "9997:9997"
environment:
show_config: true
restart: always
cm_explorer_web:
container_name: "cm_explorer_web"
depends_on:
- cm_explorer_server
image: chainmakerofficial/explorer-web:v2.3.0
ports:
- "9996:8080"
restart: always
docker-compose.yaml
使用 Nginx 代理前端应用
使用以下的 nginx 配置文件分别代理管理后台和区块链浏览器:
server {
listen 80;
server_name management.chainmaker-example.com;
location / {
proxy_pass http://127.0.0.1:9995;
}
location /chainmaker {
proxy_read_timeout 300;
proxy_pass http://127.0.0.1:9999;
client_max_body_size 0;
}
}
server {
listen 80;
server_name explorer.chainmaker-example.com;
location / {
proxy_pass http://127.0.0.1:9996;
}
location /chainmaker/ {
proxy_read_timeout 300;
proxy_pass http://127.0.0.1:9997/chainmaker;
}
location /signatures/ {
proxy_read_timeout 300;
proxy_pass http://127.0.0.1:9996/;
}
}
chainmaker.conf
添加部署脚本
添加运行服务的脚本:
cd /src/chainmaker
docker compose down && docker compose up -d --build
ln -sf /src/chainmaker/chainmaker.conf /etc/nginx/sites-enabled/chainmaker.conf
nginx -t && nginx -s reload
.scripts/start.sh
添加部署脚本:
REMOTE=$1
# 初始化文件夹
ssh $REMOTE -t "mkdir -p /src/chainmaker && /data/chainmaker/{management, explorer}"
rsync -av . $REMOTE:/src/chainmaker \
--exclude=.DS_Store \
--exclude=.git \
--exclude=build \
--exclude=node_modules
ssh $REMOTE -t < .scripts/start.sh
.scripts/deploy.sh
部署:
bash .scripts/deploy.sh <user@host>
实现一个简单的 go-sdk API 服务
由于上文提到的"问题三",因此需要使用
go-sdk
首先初始化一个 go 项目,并添加依赖:
go get -u chainmaker.org/chainmaker/sdk-go/v2@v2.3.0 go get -u github.com/gin-gonic/gin
在
main.go
package main
import (
"fmt"
"os"
"chainmaker.org/chainmaker/pb-go/v2/common"
sdk "chainmaker.org/chainmaker/sdk-go/v2"
"github.com/gin-gonic/gin"
)
const CONFIG_PATH = "/configs/sdk_config.yml"
func InitSdkClient() *sdk.ChainClient {
client, err := sdk.NewChainClient(sdk.WithConfPath(CONFIG_PATH))
if err != nil {
fmt.Printf("Initialize sdk failed: %s", err)
os.Exit(1)
return nil
}
fmt.Printf("Client initialized\n")
return client
}
func CallUserContract(
client *sdk.ChainClient,
action string,
contractName string,
method string,
params map[string]string,
) (int, map[string]string) {
kv_pairs := buildParamPairs(params)
var resp *common.TxResponse
var err error
if action == "invoke" {
resp, err = client.InvokeContract(
contractName,
method,
"",
kv_pairs,
-1,
true, // sync
)
} else {
resp, err = client.QueryContract(
contractName,
method,
kv_pairs,
-1,
)
}
if err != nil {
fmt.Printf("[ERROR] Invoke contract failed: %s", err.Error())
return 502, map[string]string{
"message": err.Error(),
}
}
if resp.Code != common.TxStatusCode_SUCCESS {
return 400, map[string]string{
"message": resp.ContractResult.Message,
}
}
return 200, map[string]string{
"message": resp.Message,
"data": string(resp.ContractResult.Result),
}
}
func buildParamPairs(params map[string]string) []*common.KeyValuePair {
var kv_pairs []*common.KeyValuePair
for k := range params {
kv_pairs = append(kv_pairs, &common.KeyValuePair{
Key: k,
Value: []byte(params[k]),
})
}
return kv_pairs
}
type CallContractParamsDto struct {
Action string `json:action`
ContractName string `json:contractName`
Method string `json:method`
Params map[string]string `json:params`
}
func StartApi(client *sdk.ChainClient) {
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "Hello, Gin!"})
})
router.POST("/contract", func(c *gin.Context) {
var dto CallContractParamsDto
if err := c.BindJSON(&dto); err != nil {
c.JSON(400, gin.H{"message": "Invalid Body"})
return
}
code, data := CallUserContract(client, dto.Action, dto.ContractName, dto.Method, dto.Params)
c.JSON(code, data)
})
fmt.Println("Listening on port :8080")
router.Run(":8080")
}
func main() {
client := InitSdkClient()
StartApi(client)
}
main.go
创建一个 Dockerfile 文件用于部署 sdk 服务。
FROM golang:1.21-bookworm as build
ENV GOPROXY=https://goproxy.cn,direct
WORKDIR /app
COPY . .
RUN go mod tidy && go build -o /app/bin/sdk.bin
FROM debian:bookworm
EXPOSE 8080
WORKDIR /app
COPY --from=build /app/bin /app
ENTRYPOINT ["/app/sdk.bin", "/configs/sdk_config.yml"]
sdk/Dockerfile
编译镜像:
docker build . -t chainmaker-sdk:v2.3.0
通过管理后台中的
区块链管理/区块链概览/下载链配置
/root/configs
/root/configs/sdk\_config.yml
./crypto-config
/configs/crypto-config
docker run \ -p 8080:8080 \ -v /root/configs:/configs \ -d chainmaker-sdk chainmaker-sdk:v2.3.0
接下来尝试使用 sdk 接口调用智能合约:
curl -X "POST" "http://127.0.0.1:8080/contract" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"action": "invoke",
"method": "increase",
"contractName": "counter"
}'
curl -X "POST" "http://127.0.0.1:8080/contract" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"action": "query",
"method": "query",
"contractName": "counter"
}'
如果得到的响应为:
{"message":"send QUERY_CONTRACT failed, all client connections are busy"}
则可能是
sdk_config.yml
nodes.tls_host_name
nodes.node_addr
nodes.tls_host_name
nodes.tls_host_name
localhost